fix(iOS): keep prebuilt Headers/ in place on a Debug/Release swap - #57814
Closed
chrfalch wants to merge 1 commit into
Closed
fix(iOS): keep prebuilt Headers/ in place on a Debug/Release swap#57814chrfalch wants to merge 1 commit into
chrfalch wants to merge 1 commit into
Conversation
An iOS Release build can fail in `PrecompileModule React` with seven `include of non-modular header inside framework module` errors, but only when the build follows a Debug/Release configuration switch (#57803). `replace-rncore-version.js` deleted and recreated `Pods/React-Core-prebuilt/Headers/` on a swap. That directory holds `module.modulemap`, which `rncore.rb` activates on every target through `-fmodule-map-file`. Nothing orders an unrelated target's dependency scan against this script phase, so a scan can run while the module map is missing; the React module is then precompiled without it and `<yoga/...>`, `<react/...>` and `<RCTDeprecation/...>` resolve non-modularly. Those headers never needed replacing. The prebuild compose job emits one set of ReactNativeHeaders for both configurations, so they are identical in the Debug and Release tarballs — only the compiled framework differs. Replace `React.xcframework` and nothing else. ## Changelog: [IOS] [FIXED] - Keep the prebuilt `Headers/` in place on a Debug/Release configuration switch so the React explicit module still resolves its module map Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D114735639. |
|
@fabriziocucci merged this pull request in df5e6f6. |
react-native-bot
pushed a commit
that referenced
this pull request
Aug 4, 2026
…7814) Summary: Fixes #57803. An iOS Release build can fail in `PrecompileModule React` with seven `include of non-modular header inside framework module` errors — but only when the build follows a Debug/Release configuration switch. `replace-rncore-version.js` deleted and recreated `Pods/React-Core-prebuilt/Headers/` on a swap. That directory holds `module.modulemap`, which `rncore.rb` activates on every target through `-fmodule-map-file`. Nothing orders an unrelated target's dependency scan against this script phase, so a scan can run while the module map is missing. The React module is then precompiled without it, and `<yoga/...>`, `<react/...>` and `<RCTDeprecation/...>` resolve non-modularly. Those headers never needed replacing. The prebuild compose job emits one set of ReactNativeHeaders for both configurations, so they are identical in the Debug and Release tarballs — only the compiled framework differs. This replaces `React.xcframework` and nothing else. ## Changelog: [IOS] [FIXED] - Keep the prebuilt `Headers/` in place on a Debug/Release configuration switch so the React explicit module still resolves its module map Pull Request resolved: #57814 Test Plan: The premise, on the published 0.87.0-rc.3 artifacts (`ios-arm64_x86_64-simulator`): | compared between the Debug and Release tarballs | result | | --- | --- | | `ReactNativeHeaders…/Headers/module.modulemap` | identical | | `React.framework/Modules/module.modulemap` | identical | | `ReactNativeHeaders…/Headers` tree (`diff -rq`) | 0 differences | | `React.framework/Headers` tree (`diff -rq`) | 0 differences | The reproducer from #57803, on Xcode 26.3 with CocoaPods 1.15.2: | build | result | | --- | --- | | 0.87.0-rc.3 | **FAIL** — exit 65, 7 errors | | 0.87.0-rc.3 + this PR | **PASS** — `** BUILD SUCCEEDED **`, 0 errors | The swap still does its job in the passing build — it logs `Replacing React-Core-prebuilt/React.xcframework`, and the installed binary is the Release one: ``` installed: 55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c rc3 release: 55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c rc3 debug: 516215801a6f8a86640aae13c2f2de1bbdb95189edf124e208f528b1497c7e4c ``` A Release→Debug swap was verified the same way. Across a swap, `Headers/module.modulemap` keeps its inode while `React.xcframework` gets a new one. ## Unit tests Adds a unit test for the script, 4 cases: correct framework installed, `Headers/module.modulemap` untouched, an Expo-generated `React-use-frameworks.modulemap` left in place, and a fail-closed case on a tarball with no `React.xcframework`. The script needed a `require.main === module` guard and one export to be importable. ``` js1 test xplat/js/react-native-github/packages/react-native/scripts/__tests__/replace-rncore-version-test.js → 4 passed, 4 total ``` The module-map case is a real regression test, not just a pin. Restoring the pre-fix delete-and-recreate makes it fail on the inode assertion while the other three keep passing: ``` ✕ leaves Headers/module.modulemap untouched Expected: 735095485 Received: 735095515 ``` That only works because the fixture tarball also ships `ReactNativeHeaders.xcframework`. Without it the pre-fix code throws its fail-closed error before reaching the assertion, so the test would go red for the wrong reason and would not actually be guarding #57803. The Expo case covers behaviour this diff removes the explicit protection for. The old save-and-restore of `React-use-frameworks.modulemap` (ada3922) is unnecessary now that only `React.xcframework` is replaced, but nothing else pins it. ## Argument parsing `yargs` parsing moved inside the `require.main === module` guard, so importing the module no longer parses `process.argv`. Verified in both directions. The command line still performs the swap end to end: ``` $ node replace-rncore-version.js -c Release -r <version> -p <podsRoot> Replacing React-Core-prebuilt/React.xcframework Updating React-Core-prebuilt/.last_build_configuration with Release Done replacing React Native prebuilt installed binary: binary-Release module.modulemap inode before=735141703 after=735141703 last_build marker: Release ``` Importing with hostile argv (`-c` collides with jest's `--config`) has no side effects: ``` $ node -e "process.argv = ['node','jest','-c','jest.config.js','--version']; require('./replace-rncore-version.js')" imported OK, exports: replaceRNCoreConfiguration ``` `arc lint` is clean on both files. Reviewed By: zeyap Differential Revision: D114735639 Pulled By: fabriziocucci fbshipit-source-id: 35ead7dae9ce7ad7160005a15ecb3975817ae728
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #57803. An iOS Release build can fail in
PrecompileModule Reactwith seveninclude of non-modular header inside framework moduleerrors — but only when the build follows a Debug/Release configuration switch, and only when the project contains a pod target that does not depend on React.replace-rncore-version.jsdeleted and recreatedPods/React-Core-prebuilt/Headers/on a swap. That directory holdsmodule.modulemap, whichrncore.rbactivates on every target through-fmodule-map-file.A pod that depends on React has a target-dependency edge that orders its module scan behind
React-Core-prebuilt's script phase. A pod with no React dependency has no such edge, so its scan is free to run before the swap. TheReactmodule is then precompiled without the module map, and<yoga/...>,<react/...>and<RCTDeprecation/...>resolve textually and trip-Wnon-modular-include-in-framework-module.Those headers never needed replacing. The prebuild compose job emits one set of ReactNativeHeaders for both configurations, so they are identical in the Debug and Release tarballs — only the compiled framework differs. This replaces
React.xcframeworkand nothing else, so the module map stays put for the whole build and the ordering stops mattering.Changelog:
[IOS] [FIXED] - Keep the prebuilt
Headers/in place on a Debug/Release configuration switch so the React explicit module still resolves its module mapTest Plan
The premise, on the published 0.87.0-rc.3 artifacts (
ios-arm64_x86_64-simulator):ReactNativeHeaders…/Headers/module.modulemapReact.framework/Modules/module.modulemapReactNativeHeaders…/Headerstree (diff -rq)React.framework/Headerstree (diff -rq)The reproducer from #57803, on Xcode 26.3 with CocoaPods 1.15.2:
** BUILD SUCCEEDED **, 0 errorsThe swap still does its job in the passing build — it logs
Replacing React-Core-prebuilt/React.xcframework, and the installed binary is the Release one:A Release→Debug swap was verified the same way. Across a swap,
Headers/module.modulemapkeeps its inode whileReact.xcframeworkgets a new one.Isolating the precondition
On stock 0.87.0-rc.3, changing only the reproducer's
IndependentPod, and reading back which targets'ScanDependenciesran before the swap:IndependentPod.podspecIndependentPod+ spec.dependency 'React-Core'Adding the dependency was the only change — same Podfile declaration, same local
:pathpod, same source file importing onlyFoundation.Note this is an ordering race, not a structural guarantee: a project with no React-independent pod is very likely safe but not provably so, since a different job count or machine load could reorder scans. This PR removes the race rather than the ordering, so the precondition stops mattering either way.
Adds a unit test for the script (3 cases: correct framework installed, module map untouched, fail on a tarball with no
React.xcframework). The script needed arequire.main === moduleguard and one export to be importable.Note for the release crew
This is a candidate for 0.87.0-rc.4. #57803 reproduces on rc.2 and rc.3 and is not fixed by anything currently on
0.87-stable; I verified #57742 in particular does not address it.Exposure: a stock template app has no React-independent pod and passed here. Apps that pull a native SDK pod directly (Firebase, Sentry, analytics or networking libraries) or vendor a local utility pod are the exposed shape, including via transitive pods of an RN wrapper.
Not fixed here:
React.xcframeworkis still replaced mid-build, and Xcode's xcframework-extraction task is not ordered against this script phase either. No failure was observed from that, and closing it properly means installing both configurations side by side instead of mutating pod content during a build — too large for a release cut.🤖 Generated with Claude Code